classdef dotsQueryableHIDGamepad < dotsQueryableHID
    % @class dotsQueryableHIDGamepad
    % Acquires and classifies data from a HID gamepad device.
    % @details
    % dotsQueryableHIDGamepad extends the dotsQueryableHID superclass to
    % manage HID devices of the gamepad type.  Gamepads have buttons, "x"
    % and "y" axes, and possibly "z" axes.
    % @details
    % dotsQueryableHIDGamepad is a usable class.  Use the newObject()
    % method of dotsTheQueryablesManager to create new
    % dotsQueryableHIDGamepad objects.
    % @details
    % By default, dotsQueryableHIDGamepad tracks value changes on its
    % button elements.  So the queryNextTracked() method can help make sure
    % that no button-related phenomenons are missed.
    % @details
    % dotsQueryableHIDGamepad does not track its axis elements by default.
    % The xValue, yValue, and zValue properties maintain the most recent
    % axis values.
    % @ingroup queryable
    
    properties
        % number of points along the x, y, and z axes.
        % The default, 3, includes the minimum, middle, and maximum from
        % each axis.
        xyzSize = [3 3 3];

        % minimum calibrated value for the x, y, and z axes.
        xyzMin = [-1 -1 -1];

        % maximum calibrated value for the x, y, and z axes.
        xyzMax = [1 1 1];
        
        % struct of HID parameters to identify the "x" axis element of the
        % gamepad.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        xMatching;
        
        % an element cookie as returned from
        % mexHID('findMatchingElements'), for the element that represents
        % the "x" axis of the gamepad.
        xCookie;
        
        % the most recent value for the "x" axis element of the gamepad.
        xValue;
        
        % struct of HID parameters to identify the "y" axis element of the
        % gamepad.
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        yMatching;
        
        % an element cookie as returned from
        % mexHID('findMatchingElements'), for the element that represents
        % the "y" axis of the gamepad.
        yCookie;
        
        % the most recent value for the "y" axis element of the gamepad.
        yValue;
        
        % struct of HID parameters to identify the "z" axis element of the
        % gamepad (if it has a "z" axis).
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        zMatching;
        
        % an element cookie as returned from
        % mexHID('findMatchingElements'), for the element that represents
        % the "z" axis of the gamepad (if it has a "z" axis).
        zCookie;
        
        % the most recent value for the "z" axis element of the gamepad.
        zValue;
        
        % array of element cookies as returned from
        % mexHID('findMatchingElements'), for the axis elements that
        % mexHID() found on the gamepad.
        axisCookies;
        
        % struct of HID parameters to identify the button elements of the
        % gamepad (it may have any number of buttons).
        % @details
        % See mexHIDUsage() for lists of valid HID parameters.  Use
        % dotsTheMachineConfiguration to specify machine-specific
        % paramers or values.
        buttonMatching;
        
        % array of element cookies as returned from
        % mexHID('findMatchingElements').
        % @details
        % Contains the button elements that mexHID() found on the gamepad
        % (it may have any number of buttons).
        buttonCookies;
    end
    
    methods
        % Constructor takes no arguments.
        % @details
        % User should use dotsTheQueryablesManager to create new objects.
        function self = dotsQueryableHIDGamepad
            self = self@dotsQueryableHID;
        end
        
        % Locate a gamepad device and its axis and button elements.
        % @details
        % Extends the dotsQueryableHID openMexHIDDevice() method to also
        % locate axis and button elements of the gamepad.
        function isOpen = openMexHIDDevice(self)
            isOpen = self.openMexHIDDevice@dotsQueryableHID;
            
            if isOpen
                % find axes
                cookie = mexHID('findMatchingElements', ...
                    self.deviceID, self.xMatching);
                self.xCookie = cookie(1);
                
                cookie = mexHID('findMatchingElements', ...
                    self.deviceID, self.yMatching);
                self.yCookie = cookie(1);
                
                cookie = mexHID('findMatchingElements', ...
                    self.deviceID, self.zMatching);
                self.zCookie = cookie(1);
                
                self.axisCookies = [self.xCookie, self.yCookie self.zCookie];
                self.axisCookies = self.axisCookies(self.axisCookies > 0);
                
                % find buttons
                %   sort by "Usage" priority
                buttonCookies = mexHID('findMatchingElements', ...
                    self.deviceID, self.buttonMatching);
                propStruct = mexHID('getElementProperties', ...
                    self.deviceID, buttonCookies, {'Usage'});
                [sorted, order] = sort([propStruct.Usage], 'ascend');
                self.buttonCookies = buttonCookies(order);
                
                self.queueCookies = [self.axisCookies, self.buttonCookies];
                isOpen = ~isempty(self.queueCookies);
            end
        end
        
        % Show a figure with continuously updated device data.
        % @details
        % Opens a new figure and plots axis and button data.  Continuously
        % updates the plot data as long as the figure is open.  Should be a
        % handy way to test or inspect a device.
        % @details
        % Shows axes values and button values in separate subplots.
        function plotData(self)
            if self.isAvailable
                
                f = figure;
                
                axisDims = self.dimensionByID(self.axisCookies);
                n = length(axisDims);
                ticks = unique([ ...
                    self.dimensions(axisDims).minValue, ...
                    self.dimensions(axisDims).maxValue, ...
                    self.dimensions(axisDims).neutralValue]);
                ax = subplot(2,1,1, 'Parent', f, ...
                    'YLim', [0, n+1], ...
                    'YTick', 1:n, ...
                    'YTickLabel', {self.dimensions(axisDims).name}, ...
                    'XLim', [min(ticks), max(ticks)+eps], ...
                    'XTick', ticks);
                ylabel(ax, 'axes');
                axisLine = line(zeros(1,n), 1:n, 'Parent', ax, ...
                    'LineStyle', 'none', 'Marker', 'x');
                
                buttonDims = self.dimensionByID(self.buttonCookies);
                n = length(buttonDims);
                ticks = unique([ ...
                    self.dimensions(buttonDims).minValue, ...
                    self.dimensions(buttonDims).maxValue, ...
                    self.dimensions(buttonDims).neutralValue]);
                ax = subplot(2,1,2, 'Parent', f, ...
                    'XLim', [0, n+1], ...
                    'XTick', 1:n, ...
                    'YLim', [min(ticks), max(ticks)+eps], ...
                    'YTick', ticks);
                xlabel(ax, 'buttons');
                buttonLine = line(1:n, zeros(1,n), 'Parent', ax, ...
                    'LineStyle', 'none', 'Marker', '.');
                
                while ishandle(f)
                    mexHID('step');
                    
                    set(axisLine, 'XData', self.currentState(axisDims,2));
                    set(buttonLine, 'YData', self.currentState(buttonDims,2));
                    
                    drawnow;
                    WaitSecs(.002);
                end
            end
        end
        
        % Add formatted data to allData and do accounting, maintain the xValue,
        % yValue, and zValue properties.
        % @param data nx3 double matrix of formatted data.
        % See the dotsQueryable allData property for the correct data
        % format.
        % @details
        % Extends the dotsQueryable appendData() method to copy the most recent
        % value from each axis into a convenient property.
        function appendData(self, newData)
            self.appendData@dotsQueryableHID(newData);
            xSelector = newData(:,1) == self.xCookie;
            if any(xSelector)
                newX = newData(xSelector,2);
                self.xValue = newX(end);
            end
            
            ySelector = newData(:,1) == self.yCookie;
            if any(ySelector)
                newY = newData(ySelector,2);
                self.yValue = newY(end);
            end
            
            zSelector = newData(:,1) == self.zCookie;
            if any(zSelector)
                newZ = newData(ZSelector,2);
                self.zValue = newZ(end);
            end
        end
    end
    
    methods (Access = protected)
        % Configre the array of dotsFiniteDimension to represent the space
        % of axes and buttons.
        function setupDimensions(self)
            nAxes = length(self.axisCookies);
            axisInfo = mexHID('summarizeElements', ...
                self.deviceID, self.axisCookies);
            for ii = 1:nAxes
                cookie = self.axisCookies(ii);
                axis = cookie == [self.xCookie, self.yCookie self.zCookie];
                
                axisName = mexHIDUsage.nameForUsageNumberOnPage( ...
                    axisInfo(ii).Usage, axisInfo(ii).UsagePage);

                % scale axes as specified
                calibrate.CalibrationMin = self.xyzMin(axis);
                calibrate.CalibrationMax = self.xyzMax(axis);
                mexHID('setElementProperties', self.deviceID, ...
                    cookie, calibrate);
                
                self.dimensions(ii) = dotsFiniteDimension.newLinear( ...
                    self.xyzMin(axis), ...
                    self.xyzMax(axis), ...
                    self.xyzSize(axis));
                self.dimensions(ii).ID = cookie;
                self.dimensions(ii).name = axisName;
                meanValue = floor(mean([ ...
                    self.dimensions(ii).minValue, ...
                    self.dimensions(ii).maxValue]));
                self.dimensions(ii).neutralValue = meanValue;
                
                % don't track axes
                self.isTrackingDimension(ii) = false;
            end
            
            nButtons = length(self.buttonCookies);
            buttonInfo = mexHID('summarizeElements', ...
                self.deviceID, self.buttonCookies);
            for ii = 1:nButtons
                jj = nAxes+ii;
                self.dimensions(jj) = dotsFiniteDimension.newLinear( ...
                    buttonInfo(ii).CalibrationMin, ...
                    buttonInfo(ii).CalibrationMax, ...
                    2);
                self.dimensions(jj).name = sprintf('button_%d', ii);
                self.dimensions(jj).ID = self.buttonCookies(ii);
                self.dimensions(jj).neutralValue = ...
                    self.dimensions(jj).minValue;

                % track buttons
                self.isTrackingDimension(jj) = true;
            end
        end
        
        % Identify handy, reusable regions in the object's data space.
        % @details
        %   - min and max for each axis
        %   - pressed and released for each button
        %   .
        function setupDefaultPhenomenons(self)
            temp = self.getPhenomenonTemplate;
            
            % for each axis, min and max
            axisDims = self.dimensionByID(self.axisCookies);
            for aa = axisDims
                d = self.dimensions(aa);
                maxed = dotsPhenomenon.partition(temp, ...
                    d.name, d.maxValue, '==');
                maxed.name = sprintf('max_%s', d.name);
                self.addPhenomenonToGroup(maxed, 'axes');
                
                mined = dotsPhenomenon.partition(temp, ...
                    d.name, d.minValue, '==');
                mined.name = sprintf('min_%s', d.name);
                self.addPhenomenonToGroup(mined, 'axes');
            end
            axesPhenoms = self.getAllPhenomenonsFromGroup('axes');
            anyAxis = dotsPhenomenon.composite([axesPhenoms{:}], 'union');
            anyAxis.name = 'any_axis';
            self.addPhenomenonToGroup(anyAxis, 'axes');
            
            % for each button, pressed and released
            buttonDims = self.dimensionByID(self.buttonCookies);
            for bb = buttonDims
                d = self.dimensions(bb);
                pressed = dotsPhenomenon.partition(temp, ...
                    d.name, d.maxValue, '==');
                pressed.name = sprintf('pressed_%s', d.name);
                self.addPhenomenonToGroup(pressed, 'pressed');
                
                released = dotsPhenomenon.complement(pressed);
                released.name = sprintf('released_%s', d.name);
                self.addPhenomenonToGroup(released, 'released');
            end
            pressedPhenoms = self.getAllPhenomenonsFromGroup('pressed');
            anyPressed = dotsPhenomenon.composite([pressedPhenoms{:}], 'union');
            anyPressed.name = 'any_pressed';
            self.addPhenomenonToGroup(anyPressed, 'pressed');
        end
    end
end